The Alchemix protocol was launched not long ago and it has generated a lot of buzz. Users can deposit DAI into Alchemix’s vault as collateral and they can mint alUSD as a loan. The best part is these loans repay themselves by generating yields in a Yearn vault. The protocol itself lives in the repository https://github.com/alchemix-finance/alchemix-protocol.
Alchemist.sol
This contract is the brain that controls every moving part. It maintains a list of every user’s collateralized debt position (CDP), a list of vaults that generate yields to repay loans and constants such as the minimum collateralization limit.
When a user wants to deposit DAI into this contract, the method deposit
is called and it does the following
Updates the CDP
// This is a user's CDP
struct Data {
uint256 totalDeposited;
uint256 totalDebt;
uint256 totalCredit;
uint256 lastDeposit;
FixedPointMath.uq192x64 lastAccumulatedYieldWeight;
}
// Get the CDP's earned yield
uint256 _earnedYield = _currentAccumulatedYieldWeight.sub(_lastAccumulatedYieldWeight).mul(_self.totalDeposited).decode();
// Adjust the CDP's debt position and credit position (if applicable)
if (_earnedYield > _self.totalDebt) {
uint256 _currentTotalDebt = _self.totalDebt;
_self.totalDebt = 0;
_self.totalCredit = _earnedYield.sub(_currentTotalDebt);
} else {
_self.totalDebt = _self.totalDebt.sub(_earnedYield);
}
// Updates the CDP's last accumulated yield weight
_self.lastAccumulatedYieldWeight = _ctx.accumulatedYieldWeight;
Transfer the deposited DAI to the Alchemist contract.
Flush active vault if the deposited amount >= flushActivator, meaning everything is swept to the yield generating vault.
// For developers coming from web2, an adapter is like an
// API client that calls a smart contract, which in this
// case can be a Yearn vault or any DeFi protocols that accept // _token as a deposit to generate yield.
_token.safeTransfer(address(_self.adapter), _amount);
_self.adapter.deposit(_amount);
_self.totalDeposited = _self.totalDeposited.add(_amount);
When a user wants to withdraw his collaterals, it follows a similar process except there are several additional checks.
It makes sure the block number is greater than the CDP last deposit’s block number to prevent flashloan attack.
require(block.number > _cdp.lastDeposit, "");
It makes sure the user’s collateralization ratio does not fall below the minimum requirement.
return _ctx.collateralizationLimit.cmp(_self.getCollateralizationRatio(_ctx)) <= 0;
// In getCollateralizationRatio...
// A CDP's collateralization ratio is calculated by dividing a vault's total deposited collaterals over its total debt. If the total debt is 0, returns the maximum value of a fixed point integer.
uint256 _totalDebt = _self.getUpdatedTotalDebt(_ctx);
if (_totalDebt == 0) {
return FixedPointMath.maximumValue();
}
return FixedPointMath.fromU256(_self.totalDeposited).div(_totalDebt);
After depositing collaterals, user can mint alUSD into existence by calling the mint
method.
// If the user tries to mint more than his total available credit, it will add the difference to the CDP as debt. Meanwhile, it has to observe the minimum collateralization ratio or else the method call will revert.
// If the user tries to mint within his available credit, the method only deducts his total credit and then mint alUSD into existence.
if (_totalCredit < _amount) {
uint256 _remainingAmount = _amount.sub(_totalCredit);
_cdp.totalDebt = _cdp.totalDebt.add(_remainingAmount);
_cdp.totalCredit = 0;
_cdp.checkHealth(_ctx, "Alchemist: Loan-to-value ratio breached");
} else {
_cdp.totalCredit = _totalCredit.sub(_amount);
}
xtoken.mint(msg.sender, _amount);
A user has the option to repay his loan with DAI or alUSD by calling the method repay
// _parentAmount is the amount in DAI
// _childAmount is the amount in alUSD
function repay(uint256 _parentAmount, uint256 _childAmount)
// When DAI is sent, transfer DAI to the Alchemist contract
// and then distribute the token to the transmuter
if (_parentAmount > 0) {
token.safeTransferFrom(msg.sender, address(this), _parentAmount);
_distributeToTransmuter(_parentAmount);
}
// When alUSD is sent, burn the alUSD and lower the CDP's alUSD minted count
if (_childAmount > 0) {
xtoken.burnFrom(msg.sender, _childAmount);
xtoken.lowerHasMinted(_childAmount);
}
// Deduct the total deposited amount (DAI + alUSD combined) from the CDP's debt position
// The protocol pegs DAI and alUSD at 1:1
uint256 _totalAmount = _parentAmount.add(_childAmount);
_cdp.totalDebt = _cdp.totalDebt.sub(_totalAmount, "");
A user can also repay his loan by liquidating his loan (without paying anything). The contract will use the user’s existing collaterals and deduct his total deposited amount and debt.
_cdp.totalDeposited = _cdp.totalDeposited.sub(_decreasedValue, "");
_cdp.totalDebt = _cdp.totalDebt.sub(_withdrawnAmount, "");
Transmuter.sol
A transmuter requires more explanations. It allows users’ deposited alUSD to be transformed into DAI over time. As the yield in DAI comes in, a user will be credited proportional to the amount of alUSD deposited. His alUSD will be burned when he decides to withdraw his DAI. For more explanation, see https://alchemix-finance.gitbook.io/alchemix-finance/transmuter
User can stake his alUSD by calling stake
// Transfer alUSD from the user to the transmuter IERC20Burnable(AlToken).safeTransferFrom(sender, address(this), amount);
// Increase total and user alUSD count
totalSupplyAltokens = totalSupplyAltokens.add(amount); depositedAlTokens[sender] = depositedAlTokens[sender].add(amount);
User can claim his DAI stored in the storage realisedTokens
by calling claim
after they have been transmuted
uint256 value = realisedTokens[sender];
realisedTokens[sender] = 0; IERC20Burnable(Token).safeTransfer(sender, value);
The process of transmutation (transmute method) involves the following:
Check pending unrealized dividends in DAI from the storage
tokensInBucket
.Make sure the pending dividends is not greater than the original deposited amount in alUSD.
Burn alUSD.
Increase the user’s
realisedTokens
so that he will be able to claim it.
Finally, a user can unstake his alUSD by calling unstake
, which removes his staked alUSD from the transmuter and sends it back to him.
depositedAlTokens[sender] = depositedAlTokens[sender].sub(amount);
totalSupplyAltokens = totalSupplyAltokens.sub(amount); IERC20Burnable(AlToken).safeTransfer(sender, amount);
In order for the yields to be available to the transmuter, the Alchemist contract has to call the distribute
method to transfer tokens to it.
IERC20Burnable(Token).safeTransferFrom(origin, address(this), amount);
buffer = buffer.add(amount);
Not all dividends can be immediately distributed. There is a transmutation period that has to be observed, which is currently set as 50 blocks. If the current block since the last deposit block has exceeded 50, the amount to be distributed is equal to the buffer. Otherwise, the amount to be distributed is equal to
// amount to distribute = buffer x (current block - last deposit block) ÷ 50 blocks
_toDistribute = _buffer.mul(deltaTime).div(TRANSMUTATION_PERIOD);
This process happens in runPhasedDistribution
, which is run whenever stake
, transmute
, forceTransmute
or distribute
happens.
The dividends owed to an account can be calculated by the method dividendsOwing
// The new dividend points is calculated from the total dividend points minus the user's last dividend points.
// The user's last dividend points is updated when his unclaimed dividends is moved to tokensInBucket
uint256 newDividendPoints = totalDividendPoints.sub(lastDividendPoints[account]);
uint256 owing = depositedAlTokens[account].mul(newDividendPoints).div(pointMultiplier);
totalDividendPoints
is increased every time when there is a distribution event or a transmutation event with excess pending dividends.
AlToken.sol
The core token of this protocol is the alUSD, which is a synthetic USD created by depositing DAI as collaterals. It lives in the contract AIToken.sol
.
It is a standard ERC-20 token except it can only be minted by a range of whitelisted addresses, as seen in the modifier onlyWhitelisted
. The admin address is the only one who can decide which addresses are whitelisted.
function setWhitelist(address _toWhitelist, bool _state) external onlyAdmin {
whiteList[_toWhitelist] = _state;
}
There is a ceiling that can be minted for each user and it is done via the following check.
uint256 _total = _amount.add(hasMinted[msg.sender]); require(_total <= ceiling[msg.sender],"AlUSD: Alchemist's ceiling was breached.");
Finally, the part where you degens care the most, REWARDS.
StakingPools.sol
This contract stores a list of staking pools as well as controls rewards. It is a governance contract where most methods can only be called by addresses with the governance role, as seen here.
modifier onlyGovernance() {
require(msg.sender == governance, "StakingPools: only governance");
_;
}
Governance can do a variety of things, such as setting the reward rate.
function setRewardRate(uint256 _rewardRate) external onlyGovernance {
_updatePools();
_ctx.rewardRate = _rewardRate;
emit RewardRateUpdated(_rewardRate);
}
The contract has different reward pools and the governance address is allowed to create a new reward pool by calling createPool
, which contains the following columns
struct Data {
IERC20 token;
uint256 totalDeposited;
uint256 rewardWeight;
FixedPointMath.uq192x64 accumulatedRewardWeight;
uint256 lastUpdatedBlock;
}
token
refers to the token awarded to farmers, which in this case is ALCX (AlchemixToken.sol).totalDeposited
is the total amount of staked tokens,rewardWeight
refers to a pool’s reward percentage out of all pools. Reward weights were recently updated after this proposal passed. They can be updated all at once using the methodsetRewardWeights
.accumulatedRewardWeight
is the reward weight accumulated every time a pool is updated, with the formula.
uint256 _rewardRate = _data.getRewardRate(_ctx);
uint256 _distributeAmount = _rewardRate.mul(_elapsedTime);
FixedPointMath.uq192x64 memory _rewardWeight = FixedPointMath.fromU256(_distributeAmount).div(_data.totalDeposited);
lastUpdatedBlock
is the the block number when the pool was updated.
Users can stake tokens to any staking pools by calling the method deposit
, which adds the deposit amount to the pool and the stake, and then transfers the staked tokens to the staking pool contract.
_pool.totalDeposited = _pool.totalDeposited.add(_depositAmount);
_stake.totalDeposited = _stake.totalDeposited.add(_depositAmount);
_pool.token.safeTransferFrom(msg.sender, address(this), _depositAmount);
Withdrawing from the staking pool is similar to depositing, except _depositAmount
is deducted, the staked tokens are transferred back to the user and the unclaimed rewards are minted in order to be transferred to the user.
uint256 _claimAmount = _stake.totalUnclaimed;
_stake.totalUnclaimed = 0;
reward.mint(msg.sender, _claimAmount);
It is a good practice to always first set the amount to be transferred to 0 before minting/transferring it. Leaky contracts might allow hackers repeatedly mint the rewards with no limit.
Note that in many methods the modifier nonReentrant
is used, this is to prevent the method from being called repeatedly for malicious gains (see the infamous DAO hack in 2016), especially if the method calls an external contract which in turn can call back the originating contract’s method recursively. OpenZeppelin’s reentrancy guard looks like this.
modifier nonReentrant() {
require(!rentrancy_lock);
rentrancy_lock = true;
_;
rentrancy_lock = false;
}
Ser where do you see the magic. the protocol looks quite standard, or I'm missing something?