---
# System prepended metadata

title: DOT LSD Farming

---

# 1. 可升级合约方案
原理是将入口合约(proxy contract)和执行合约(implement contract)分离，使用delegate 将对入口合约的调用，选择性转发给 implement contract 执行。proxy contract可以替换 implement contract, 存储始终都在proxy contract中。 

实现方式有很多参考：
1. transparent proxy 方案， 经典的方案, 理解上比较直观 [https://blog.openzeppelin.com/the-transparent-proxy-pattern](https://blog.openzeppelin.com/the-transparent-proxy-pattern)
2.  UUPS proxy方案， 与transparent proxy方案最大区别在于升级的控制逻辑放在impl contract， 意味着一个可升级合约可以升级成 不可变 合约 [https://eips.ethereum.org/EIPS/eip-1822](https://eips.ethereum.org/EIPS/eip-1822)
3. diamonds 方案， diamonds 看上去比较先进的方案，解决了前两者的一些问题，但分离的粒度过细， 前端也要做更多的兼容。[https://eips.ethereum.org/EIPS/eip-2535](https://eips.ethereum.org/EIPS/eip-2535)

综合来看，先排除方案3吧，可参考的例子比1，2要少。 2 的优点是可升级合约保留了升级成 不可变合约的可能， 同时意味着implemention contract的迭代开发要注意升级逻辑的保留，这也是一个风险点。 我更偏向使用更为稳妥的 transparent proxy 方案

其他要点
1. Proxy Pattern 使用 transparent proxy 模式,  owner 的调用不进行转发， 非 owner 的调用始终转发给 implemention contract [参考](https://blog.openzeppelin.com/the-transparent-proxy-pattern)
2.  proxy contract 使用非结构化的存储作为 proxy contract 的存储索引， 避免与 implemention contract 的 storage 混淆 [参考](https://blog.openzeppelin.com/upgradeability-using-unstructured-storage)

这是一个相对完整的示例： [https://solidity-by-example.org/app/upgradeable-proxy/](https://solidity-by-example.org/app/upgradeable-proxy/)

# 2. Implemention Contract 设计

要点：  

1. implemention contract 及其父合约中不要有 `constructor`, 请将引入作为父合约的open-zepplin 合约，替换成[https://github.com/OpenZeppelin/openzeppelin-upgrades/]( (https://github.com/OpenZeppelin/openzeppelin-upgrades/) , 使用 initializer 来替换掉原本的 ``constructor`。
2. implemention contract 中不要有 `selfdestruct` 和 `delegatecall`
3. 如果不接受原生native token， 可以不用实现 fallback 和 receive.  (使用 ERC20形式的ACA, KAR)
4. 引入 SafeMath 和 SafeERC20 等libray
5. implemention contract 中定义 storage， 在实现新的 implemention contract 时：  代码中不要对旧版本定义的storage进行删除，重新排序， 更改类型，插入新storage等等。若要添加新的storage， 附加在已有storage之后。 请将所有的 storage 统一在 storage contract 中定义， 后续不同版本 implemention contract 可以继承自 storage contract，新版本 storage contract 可以继承老版本的 storage contract。 


业务层面要点：

1.  LSD转化的farming合约 有些特性， 比如LcDOT何时转化成LDOT或tDOT， 何时转化，转化的比例， 而且在转化后，目前看来不会再新开LcDOT池子。 考虑和 通用farming 合约可以独立成两套设计分别进行部署
2. 由contract owner 进行创建池子，调整奖励，以及针对每个池子进行校验。 后续如果要改成开放式的，允许任意用户创建pool和奖励。再做升级进行支持。那目前的设计中，奖励是要先单独 transfer 给 implemention contract 地址， 我们校验资金到位后，来开启/调整。 
3. 针对指定pool可以单独关闭 stake，unstake， claimRewards 的操作。
4. 目前的灼烧机制是针对某个池子的所有奖励类型。
5. 正常版本不用引入后门供owner 转移implemention contract 地址的资金， 合约引入了Pausable， 可以在有这个需求时，pause掉合约，再升级成owner可以转移资金的版本。 


合约设计：

```
contract LsdStaking is Ownable, Pausable {	// 继承 open-zeppelin Ownable 合约，合约具有所有权，所有权限制和转移所有权等功能
	//  继承 open-zeppelin Pausable 合约， 合约具有关停的控制权限
	/****************************** types defination ******************************/
	
	// 池子信息
	struct PoolInfo {
		IERC20 shareToken; // share token 的 ERC20 地址
		uint256 totalShare; // 池子当前质押的 share 总量
		uint256   rewardsDeductionRate; // 奖励灼烧比例， 1e18 是 100%
	}
	
	// 奖励规则
	Struct RewardRule {
       uint256 rewardRate;	// 每秒钟奖励token的数量
		uint256 endTime;	// 奖励累计的截止时间
		uint256 rewardRateAccumulated;  // share 对奖励token 的兑换比例
		uint256 lastRewardAccumulatedTime; // 上次结算奖励的时间戳
	}
	
	enum UserOperation { Stake, Unstake, ClaimRewards }
	
	/****************************** storages defination ******************************/
	
	// 池子索引
	uint256 poolIndex = 0;
	
	// 用户行为限制
	mapping(uint256 => mapping(UserOperation => bool)) prohibitedOperations;
	
	// 池子信息
	// KEY_TYPE: 池子索引 
	// VALUE_TYPE: 池子信息
	mapping(uint256 => PoolInfo)  pools;
	
	// 池子的奖励token种类(当前正在奖励及曾经奖励过的)
	// KEY_TYPE: 池子索引 
	// VALUE_TYPE: 奖励erc20 类型数组
	mapping(uint256 => IERC20[]) rewardTokens;
	
	// 奖励规则
	// KEY_0_TYPE: 池子索引 
	// KEY_1_TYPE: 奖励erc20类型 
	// VALUE_TYPE: 奖励规则和信息
	mapping(uint256 => mapping(IERC20 => RewardRule)) rewardRules;
	
	// 用户质押在池子的share数量
	// KEY_0_TYPE: 池子索引 
	// KEY_1_TYPE: 用户地址
	// VALUE_TYPE: 质押token的数量
	mapping(uint256 => mapping(address => uin256)) stakedShares
	
	// 用户待领取的奖励
	// KEY_0_TYPE: 池子索引 
	// KEY_1_TYPE: 用户地址
	// KEY_2_TYPE: 奖励erc20类型
	// VALUE_TYPE: 已结算的待领取奖励数量
	mapping(uint256 => mapping(address => mapping(IERC20 => uint256))) rewards;
	
	
	// 已经支付给用户的 每share兑奖励数量的比例
	// KEY_0_TYPE: 池子索引 
	// KEY_1_TYPE: 用户地址
	// KEY_2_TYPE: 奖励erc20类型
	// VALUE_TYPE: 用户share 兑奖励token 的兑换比例
	mapping(uint256 => mapping(address => mapping(IERC20 => uint256))) userRewardPerSharePaid;
	
	/****************************** constructor or initializer ******************************/
	constructor(address owner) Ownable(owner) {}
	
	/****************************** impl external functions for Pausable ******************************/
	function pause() external onlyOwner {
        Pausable._paused();
    }

    function unpause() external onlyOwner {
    	 Pausable._unpaused();
    }
	
	/****************************** view functions defination ******************************/

	// reward需要进行accumulate的 最新时间戳,  如果当前时间在奖励结束之前， 返回当前时间，
	// 如果已经结束，则返回结束时间
	function lastTimeRewardApplicable(uint256 poolId, IERC20 rewardType) public view returns (uint256) {
		uint256 endTime = rewardRules(pool.id, rewardType).endTime;
		return block.timestamp < endTime ? block.timestamp : endTime;
	}
	
	// 当前某池子的share 兑 某erc20 奖励的兑换比例(已经accumulated的部分 + 待accumulated的部分)
	function rewardPerShare(uint256 poolId, IERC20 rewardType) public view returns (uint256) {
		uint256 totalShare = poolInfo(poolId).totalShare;
		uint256 lastRewardAccumulatedTime = rewardRules(poolId, rewardType).lastRewardAccumulatedTime;
		uint256 rewardRate = rewardRules(poolId, rewardType).rewardRate;
		uint256 rewardRateAccumulated = rewardRules(poolId, rewardType). rewardRateAccumulated;
		
		if (totalShare == 0) {
			return rewardRateAccumulated;
		}
		
		uint256 pendingRewardRate = lastTimeRewardApplicable(poolId, rewardType).sub(lastRewardAccumulatedTime).mul(rewardRate).mul(1e18).div(totalShare); // * 10**18 来防止精度丢失
		
		return rewardRateAccumulated.add(pendingRewardRate);
	}
    
	// 用户在某个池子某个奖励币种下的待领取奖励数量
	function earned(uint256 poolId, address staker, IERC20 rewardType) public view returns (uint256) {
		uint256 stakedShare = stakedShares(poolId, staker);
		uint256 userRewardPerSharePaid = userRewardPerSharePaid(poolId, staker, rewardType);
		uint256 rewardPerShare = rewardPerShare(poolId, rewardType);
		uint256 reward = rewards(poolId, staker, IERC20);
		uint256 pendingReward = stakedShare.mul(rewardPerShare.sub(userRewardPerSharePaid)).div(1e18); // / 10**18 对应上面的精度处理
		
		return reward.add(pendingReward);
	}
	
	/****************************** modifiers defination  ******************************/
	
	modifier operationNotProhibited(uint256 poolId, UserOperation operation) {
		require(
            prohibitedOperations(poolId, operation) == false,
            "The pool prohibited this operation."
       );
       _;
	}
	
	// 奖励更新，此 modifier 请在 质押/解绑/领取奖励， 及奖励计划发生变化的function中包含
	modifier updateRewards(uint256 poolId, address account) {
		// 获取pool下所有存在过的奖励类型
		IERC20[] rewardTypes = rewardTokens(poolId);
		
		for (uint i = 0; i < rewardTypes.length; i++) {
			RewardRule storage rewardRule = rewardRules[poolId][rewardTypes[i]];
			
			rewardRule.rewardRateAccumulated = rewardPerShare(poolId, rewardTypes[i]);
			rewardRule.lastRewardAccumulatedTime = lastTimeRewardApplicable(poolId, rewardTypes[i]);
			
			// account 是非0地址的话， 还需要更新对该 account 的奖励进行结算和distribute
			if (account != address(0)) {
				rewards[poolId][account][rewardTypes[i]] = earned(poolId, account, rewardTypes[i]);
				userRewardPerSharePaid[poolId][account][rewardTypes[i]] = rewardRule.rewardRateAccumulated;
			}
		}
		_;
	}
	
	/****************************** owner才有权限的调用  ******************************/
	// 设置限制行为
	function setProhibitedOperation(uint256 poolId, UserOperation, prohibite bool) public onlyOwner {
		// do not check poolId, so that can set in advance.
		prohibitedOperations[poolId][UserOperation] = prohibite;
		emit ;
	}
	
	// 创建新的池子
	function initializePool(IERC20 shareToken) public onlyOwner {
		require(shareToken != address(0), "0 address not allowed")
		pools[poolIndex()] = PoolInfo
		
		// 修改索引
		poolIndex = poolIndex.add(1);
		
		emit Event;
	}
	
	function setRewardsDeductionRate(uint256 poolId, uint256 rate) public onlyOwner {
		require(pools(poolId).shareToken != address(0), "Invalid pool");
		require(rate <= 1e18, "invalid rate");
		
		pools[poolId].rewardsDeductionRate = rate;
		emit Event;
	}
	
	// 奖励计划变动， 但只覆盖下面三种情况
	// 1. 原有奖励已经结束，新增奖励
	// 2. 原有奖励尚未结束， 新增奖励为0， 原有奖励剩余部分按 rewardDuration 设定新的奖励速率和新的结束时间
	// 3. 原有奖励尚未结束， 原有奖励剩余部分加上新增奖励， 按 rewardDuration 设定新的结束时间和奖励速率
	// TBD: 不包含对原有奖励剩余部分的调整， 是有需要加上这种形式的调整
	function notifyRewardRule(
		uint256 poolId, 
		IERC20 rewardType, 
		uint256 rewardAmount, 
		uint256 rewardDuration,
	) public onlyOwner updateRewards(poolId, address(0)) {
		// checks
		require(rewardType != address(0), "not allowed");
		require(pools(poolId).shareToken != address(0), "pool must exist");
		
		// 如果是新奖励币种， 需要添加到 rewardTokens 中
		IERC20[] memory rewardTypes = rewardTokens(poolId);
		bool isNew = ture;
		for (uint i = 0; i < rewardTypes.length; i++) {
			if (rewardTypes[i] == rewardType) {
				isNew = false;
				break;
			}
		}
		if (isNew) {
			rewardTokens[poolId].push(rewardType);
		}
	
		RewardRule storage rewardRule = rewardRules[poolId][rewardType];
		
		// 新的奖励周期
		if (block.timestamp >= rewardRule.endTime) {
			rewardRule.rewardRate = rewardAmount.div(rewardDuration);
		} else {
			// 调整奖励，将尚未distributed的旧奖励 加上 新增的 rewardAmount， 然后 按新的 rewardDuration 计算新的奖励速率 rewardRate
			uint256 remaining = rewardRule.endTime.sub(block.timestamp);
			uint256 leftover = remaining.mul(rewardRule.rewardRate);
			rewardRule.rewardRate = rewardAmount.add(leftover).div(rewardDuration);
		}
		
		// 用新的结束时间覆盖
		rewardRule.endTime = block.timestamp.add(rewardDuration);
		// 已经通过 modifier updateRewards 更新过奖励， 但为了防止老的奖励计划是已经过期的，这里必须用当前时间戳覆盖
		rewardRule.lastRewardAccumulatedTime = block.timestamp;
		
		emit RewardRuleUpdated(...);
	}
	
	/****************************** 所有用户可以进行的调用 ******************************/
	
	// 向指定池子质押指定数量的share token
	// updateRewards将先对用户pending的奖励进行accumulate
	// 将shareToken从用户地址转至合约地址
	// 更新pool totalShare 和 用户 stakedShares 存储
	function stake(uint256 poolId, uint256 amount) public whenNotPaused() operationNotProhibited(poolId, Stake) updateRewards(poolId, msg.sender) {
		require(amount > 0, "Cannot stake 0");
		IERC20 shareToken = poolInfo(poolId]).shareToken;
		require(shareToken != address(0), "Invalid pool");
		
		// 将质押token转至合约
		shareToken.safeTransferFrom(msg.sender, address(this), amount);
		
		// 更新storage
		poolInfo[poolId].shareToken = poolInfo[poolId].shareToken.add(amount);
		stakedShares[poolId][msg.sender] = stakedShares[poolId][msg.sender].add(amount);
		
		emit Staked(...);
	}
	
	// 向指定池子提取指定数量的share token
	// updateRewards将先对用户pending的奖励进行accumulate
	// 更新pool totalShare 和 用户 stakedShares 存储
	// 将shareToken转给用户地址
	function unstake(uint256 poolId, uint256 amount) public whenNotPaused() operationNotProhibited(poolId, Unstake) updateRewards(poolId, msg.sender) {
		require(amount > 0, "Cannot unstake 0");
		IERC20 shareToken = poolInfo(poolId]).shareToken;
		require(shareToken != address(0), "Invalid pool");
		
		// 更新storage
		poolInfo[poolId].shareToken = poolInfo[poolId].shareToken.sub(amount);
		stakedShares[poolId][msg.sender] = stakedShares[poolId][msg.sender].sub(amount);
		
		// 将质押token从合约转给用户
		shareToken.safeTransfer(msg.sender, amount);
		
		emit Unstaked(...);
	}
	
	// 提取指定池子的所有奖励
	// updateRewards将先对用户pending的奖励进行accumulate
	// 更新待领取奖励及转账奖励给用户
	function claimRewards(uint256 poolId) public whenNotPaused() operationNotProhibited(poolId, ClaimRewards) updateRewards(poolId, msg.sender) {
		IERC20[] rewardTypes = rewardTokens(poolId);
		uint256 rewardsDeductionRate = pools[poolId].rewardsDeductionRate;

		for (uint256 i = 0; i < rewardTypes.length; ++i) {
			uint256 rewardAmount = rewards(poolId, msg.sender, rewardTypes[i]);
			if (rewardAmount > 0) {
				// 更新storage
				rewards[poolId][msg.sender][rewardTypes[i]] = 0;
				
				uint256 deduction = rewardAmount.mul(rewardsDeductionRate).div(1e18);
				if deduction > 0 {
					RewardRule storage rewardRule = rewardRules[poolId][rewardTypes[i]];
					uint256 remainingTime = rewardRule.endTime.sub(rewardRule.lastRewardAccumulatedTime);
					uint256 addRewardRate = deduction.div(remainingTime);
					rewardRule.rewardRate = rewardRule.rewardRat.add(addRewardRate);
				}
				
				// 转账 reward
				rewardTypes[i].safeTransfer(msg.sender, rewardAmount.sub(deduction));
				emit ClaimReward(...);
			}
		}
	}
	
	
	// 用户从指定池子 unstake 所有的share 及 提取所有奖励
	function exit(uint256 poolId) external whenNotPaused() {
		unstake(poolId, stakedShares(poolId, msg.sender));
		claimRewards(poolId);
	}
	
	/****************************** events defination  ******************************/
	// TODO:
}
```

LcDOT转化LSD版本：


```

// 转化信息
struct ConvertedInfo {
	IERC20 convertedShareToken; 
	uint256 convertedExchangeRate; // 旧的shareToken 同转化后的 shareToken 的兑换比例， 1e18 是 100%
}

enum ConvertType { Lcdot2Ldot, Lcdot2Tdot }


/****************************** extra constants and storages defination ******************************/

IERC20 public constant DOT_ADDRESS = ;
IERC20 public constant LCDOT_ADDRESS = ;
IERC20 public constant LDOT_ADDRESS = ;
IERC20 public constant TDOT_ADDRESS = ;
IHoma public constant HOMA_CONTRACT = ;
IStableAsset public constant STABLE_ASSET_CONTRACT = ;

	
// share 转化信息
mapping(uint256 => ConvertedInfo) convertedInfo;


/****************************** extra function defination or function overwrite ******************************/

// 转化LSD
function initializePool(uint256 poolId, ConvertType convertType) external onlyOwner {
	if (convertType == ConvertType.Lcdot2Ldot) {
		require(pools(poolId).shareToken == LCDOT_ADDRESS, "Share token must be Lcdot");
		
		ConvertedInfo storage convert = convertedInfo[poolId];
		
		require(convert.convertedShareToken == address(0), "Already converted");
		
		uint256 beforeLdotAmount = LDOT_CONTRACT.balanceOf(address(this));
		
		// TODO: call the crowndloan contract to convert LcDOT to DOT; 
		uint256 amount = pools(poolId).totalShare;
		uin256 exchangeRate = HOMA_CONTRACT.getExchangeRate();
		require(exchangeRate != 0, "exchange rate shouldn't be zero");
		
		HOMA_CONTRACT.mint(amount);
		uint256 afterLdotAmount = LDOT_ADDRESS.balanceOf(address(this));
		
		require(amount.mul(exchangeRate).div(1e18).add(beforeLdotAmount) < afterLdotAmount, "invalid exchange rate");
		
		convert = ConvertedInfo(LDOT_ADDRESS, exchangeRate);	
		emit Event;
	} else if (convertType == ConvertType.Lcdot2Tdot) {
		// TODO:
	}
}


function stake(uint256 poolId, uint256 amount) public whenNotPaused() operationNotProhibited(poolId, Stake) updateRewards(poolId, msg.sender) {
	require(amount > 0, "Cannot stake 0");
	IERC20 shareToken = poolInfo(poolId]).shareToken;
	require(shareToken != address(0), "Invalid pool");
	
	ConvertedShare memory maybeConverted = convertedShare[poolId];
	
	if (maybeConverted.convertedShareToken != address(0)) {
		uint256 convertedAmount = amount.mul(maybeConverted.convertedExchangeRate).div(1e18);
		require(convertedAmount != 0, "shouldn't has 0 exchangeRate");
		
		maybeConverted.convertedShareToken.safeTransferFrom(msg.sender, address(this), convertedAmount);
	} else {
		// 将质押token转至合约
		shareToken.safeTransferFrom(msg.sender, address(this), amount);
	}
	
	// 更新storage
	poolInfo[poolId].shareToken = poolInfo[poolId].shareToken.add(amount);
	stakedShares[poolId][msg.sender] = stakedShares[poolId][msg.sender].add(amount);
	
	emit Staked(...);
}

function unstake(uint256 poolId, uint256 amount) public whenNotPaused() operationNotProhibited(poolId, Unstake) updateRewards(poolId, msg.sender) {
	require(amount > 0, "Cannot unstake 0");
	IERC20 shareToken = poolInfo(poolId]).shareToken;
	require(shareToken != address(0), "Invalid pool");
	
	// 更新storage
	poolInfo[poolId].shareToken = poolInfo[poolId].shareToken.sub(amount);
	stakedShares[poolId][msg.sender] = stakedShares[poolId][msg.sender].sub(amount);
	
	ConvertedShare memory maybeConverted = convertedShare[poolId];
	
	if (maybeConverted.convertedShareToken != address(0)) {
		maybeConverted.convertedShareToken.safeTransfer(msg.sender, amount.mul(maybeConverted.convertedExchangeRate).div(1e18));
	} else {
		// 将质押token从合约转给用户
		shareToken.safeTransfer(msg.sender, amount);
	}
	
	emit Unstaked(...);
}

```


# 3. 权限管理和治理
我们可以使用 多签合约 来作为 proxy contract 和 LsdStaking 合约中的各种 admin/owner。 多签合约按触发和校验方式多为两种：

1. member 可以线下对内容进行签名， 任意member只要汇总超过threshold的签名，提交给合约就能触发执行。合约内校验签名内容， 可以做到不触网，但需要链下的沟通和一些工具来帮助member治理
2. member 可以发起propose， 包含调用内容， 其余成员可以进行 vote， 超过threshold后触发执行。 相比1， 需要member触网， 但优点是有效性都由evm校验， propose， vote的内容都是可见的，通过一些前端工具，体验会类似于现在 Acala的 council 治理。 这里有一个多签合约的实现 [https://solidity-by-example.org/app/multi-sig-wallet/](https://solidity-by-example.org/app/multi-sig-wallet/)

