第三章:存款逻辑实现

好,咱们今天来聊聊存款逻辑。这是借贷协议最基础、也是最核心的一块。你想想看,没有存款,哪来的钱借出去?所以这块逻辑必须稳如泰山。

3.1 存款函数设计

存款函数,说白了就是用户把资产放进协议里。但这里有个关键点——我们不是直接收走用户的代币,而是发行一种凭证。我个人习惯把存款函数设计成这样:

function deposit(address asset, uint256 amount, address onBehalfOf) external returns (uint256) {
    // 1. 检查资产是否被支持
    require(assetIsSupported[asset], "Asset not supported");
    
    // 2. 从用户账户转入资产
    IERC20(asset).safeTransferFrom(msg.sender, address(this), amount);
    
    // 3. 计算应铸造的aToken数量
    uint256 aTokenAmount = amount;
    if (totalSupply[asset] > 0) {
        aTokenAmount = amount * totalATokenSupply[asset] / totalCash[asset];
    }
    
    // 4. 铸造aToken给用户
    _mint(onBehalfOf, aTokenAmount);
    
    // 5. 更新利率
    _updateInterestRate(asset);
    
    // 6. 发送事件
    emit Deposit(asset, msg.sender, onBehalfOf, amount, aTokenAmount);
    
    return aTokenAmount;
}

嗯,这里要注意一点。我见过不少新手直接把 amount 当作 aToken 数量来铸造。这其实不对。为什么?因为利率在变,你的存款凭证价值也在变。我们得用当前汇率来算。

核心设计原则:存款函数必须保证用户存入的资产和铸造的凭证之间存在正确的价值映射关系。不能多铸,也不能少铸。

3.2 aToken/份额代币铸造

aToken 是什么?它就是你的存款凭证。你存了100 USDT,协议给你100 aUSDT。但过了一个月,这100 aUSDT可能能赎回105 USDT了。这就是利息的魔力。

铸造逻辑其实不复杂,但有个坑——精度问题。我曾经在项目里遇到过,因为精度处理不当,导致用户存款后aToken数量对不上账。那叫一个头疼。

function _mint(address account, uint256 amount) internal {
    require(account != address(0), "Mint to zero address");
    
    // 更新总供应量
    totalATokenSupply += amount;
    
    // 更新用户余额
    balanceOf[account] += amount;
    
    // 这里有个小细节:记录用户存款时的汇率
    userIndex[account] = liquidityIndex;
    
    emit Transfer(address(0), account, amount);
}

我的经验:铸造aToken时,一定要记录用户当时的汇率索引(liquidityIndex)。这样后续计算利息时,才能准确算出用户赚了多少。

3.3 存款利率更新

利率更新这块,很多人觉得简单——不就是算个百分比吗?其实不然。利率是动态的,它取决于资金利用率。

资金利用率 = 总借款 / 总存款。这个值越高,利率就越高。为什么?因为钱都被借走了,新用户想存钱,自然要给出更高的利息来吸引。

function _updateInterestRate(address asset) internal {
    uint256 totalBorrows = totalBorrows[asset];
    uint256 totalLiquidity = totalCash[asset] + totalBorrows;
    
    // 计算资金利用率
    uint256 utilizationRate = totalLiquidity == 0 ? 0 : 
        totalBorrows * RAY / totalLiquidity;
    
    // 根据利用率计算新利率
    uint256 newRate;
    if (utilizationRate < OPTIMAL_UTILIZATION_RATE) {
        // 低于最优利用率,利率缓慢上升
        newRate = BASE_RATE + utilizationRate * SLOPE_1 / OPTIMAL_UTILIZATION_RATE;
    } else {
        // 高于最优利用率,利率快速上升
        newRate = BASE_RATE + SLOPE_1 + 
            (utilizationRate - OPTIMAL_UTILIZATION_RATE) * SLOPE_2 / (RAY - OPTIMAL_UTILIZATION_RATE);
    }
    
    // 更新全局利率
    liquidityIndex = liquidityIndex * (RAY + newRate * blockTimeDelta / SECONDS_PER_YEAR) / RAY;
    currentLiquidityRate = newRate;
}

避坑指南:我曾经在更新利率时忘记考虑时间差(blockTimeDelta),结果利率更新频率不对,导致用户利息计算偏差。记住,每次存款、取款、借款、还款都要触发利率更新。

3.4 存款事件与索引

事件是区块链的日志系统。每次存款,我们都要记录下关键信息。这样前端、监控系统、数据分析工具才能正常工作。

event Deposit(
    address indexed asset,      // 存款资产地址
    address indexed user,       // 实际存款人
    address indexed onBehalfOf, // 代存人(可以是自己)
    uint256 amount,             // 存款金额
    uint256 aTokenAmount,       // 铸造的aToken数量
    uint256 referralCode        // 推荐码(可选)
);

这里有个设计细节——为什么要有 onBehalfOf?说白了就是允许你帮别人存。这在DeFi里很常见,比如合约代用户操作,或者做聚合器。

索引字段的选择也很讲究。我一般会把 assetuseronBehalfOf 都设为 indexed。这样查询效率高,但要注意 gas 成本——indexed 字段越多,gas 越高。

字段 是否索引 说明
asset 方便按资产筛选存款记录
user 方便查询用户的所有存款
onBehalfOf 方便查询代存记录
amount 数值字段,不需要索引
aTokenAmount 同上

总结一下:存款逻辑看似简单,但涉及资产安全、利率计算、事件索引等多个关键点。每一步都要谨慎设计。我见过太多项目因为存款逻辑的漏洞导致资产损失,所以这块一定要反复测试。

好了,存款逻辑就讲到这里。下一章咱们聊聊借款逻辑,那才是真正考验协议设计能力的地方。