Dissecting DeFi Protocols

Share this post

Dissecting the Alchemix protocol

0xkowloon.substack.com

Dissecting the Alchemix protocol

This shit is dark magic.

0xkowloon
Mar 13, 2021
5
Share this post

Dissecting the Alchemix protocol

0xkowloon.substack.com
Besides dark magic, Alchemix also makes insanely captivating animations.

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

  1. 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;
  1. Transfer the deposited DAI to the Alchemist contract.

  2. 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.

  1. 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, "");
  1. 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:

  1. Check pending unrealized dividends in DAI from the storage tokensInBucket.

  2. Make sure the pending dividends is not greater than the original deposited amount in alUSD.

  3. Burn alUSD.

  4. 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 method setRewardWeights.

  • 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;
}
Share this post

Dissecting the Alchemix protocol

0xkowloon.substack.com
Comments
TopNewCommunity

No posts

Ready for more?

© 2023 0xkowloon
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing