neo-devpack-solidity 0.22.0

Production-focused Solidity-to-NeoVM compilation system
Documentation
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

/**
 * @title SimpleLending — Compound-style Lending Pool
 * @notice Neo N3 adaptation of Compound/Aave lending (simplified).
 *
 * Single-asset GAS lending pool with 150% collateral ratio,
 * simple per-block interest accrual, and liquidation.
 *
 * Compiler constraints respected:
 *   - No {value: ...} — uses NativeCalls.gasTransfer()
 *   - No receive()/fallback() — uses onNEP17Payment()
 *   - Import devpack via -I devpack
 */
contract SimpleLending {
    address private constant GAS_TOKEN = NativeCalls.GAS_CONTRACT;

    /// @notice Collateral ratio: 150% (scaled by 100).
    uint256 public constant COLLATERAL_RATIO = 150;

    /// @notice Liquidation bonus: 5% (scaled by 100).
    uint256 public constant LIQUIDATION_BONUS = 5;

    /// @notice Interest rate per block in basis points (1 = 0.01%).
    uint256 public constant INTEREST_RATE_PER_BLOCK = 1;
    uint256 private constant BASIS_POINTS = 10000;

    struct UserAccount {
        uint256 deposited;
        uint256 borrowed;
        uint256 lastBlock;
    }

    mapping(address => UserAccount) public accounts;
    uint256 public totalDeposited;
    uint256 public totalBorrowed;

    event Deposited(address indexed user, uint256 amount);
    event Withdrawn(address indexed user, uint256 amount);
    event Borrowed(address indexed user, uint256 amount);
    event Repaid(address indexed user, uint256 amount);
    event Liquidated(address indexed borrower, address indexed liquidator, uint256 debtRepaid, uint256 collateralSeized);

    /// @notice Tracks addresses that have signalled intent to repay.
    mapping(address => bool) private pendingRepay;

    /// @notice Signal that the next GAS transfer is a repayment (call before GAS.transfer).
    function prepareRepay() external {
        require(accounts[msg.sender].borrowed > 0, "Lending: no debt");
        pendingRepay[msg.sender] = true;
    }

    /// @notice NEP-17 callback — receives GAS deposits or repayments.
    /// Call prepareRepay() before transferring GAS to trigger repayment.
    function onNEP17Payment(address from, uint256 amount, Any calldata /*data*/) external {
        address caller = Syscalls.getCallingScriptHash();
        require(caller == GAS_TOKEN, "Lending: only GAS accepted");
        require(amount > 0, "Lending: zero amount");

        if (pendingRepay[from]) {
            pendingRepay[from] = false;
            _repay(from, amount);
        } else {
            _deposit(from, amount);
        }
    }

    function _deposit(address user, uint256 amount) internal {
        _accrueInterest(user);
        accounts[user].deposited += amount;
        totalDeposited += amount;
        emit Deposited(user, amount);
    }

    /// @notice Withdraw deposited GAS.
    function withdraw(uint256 amount) external {
        _accrueInterest(msg.sender);
        UserAccount storage acct = accounts[msg.sender];

        require(acct.deposited >= amount, "Lending: insufficient deposit");

        // Ensure remaining collateral covers debt.
        uint256 remaining = acct.deposited - amount;
        require(
            remaining * 100 >= acct.borrowed * COLLATERAL_RATIO,
            "Lending: undercollateralized"
        );

        acct.deposited -= amount;
        totalDeposited -= amount;

        bool ok = NativeCalls.gasTransfer(address(this), msg.sender, amount, "");
        require(ok, "Lending: transfer failed");

        emit Withdrawn(msg.sender, amount);
    }

    /// @notice Borrow GAS against deposited collateral.
    function borrow(uint256 amount) external {
        require(amount > 0, "Lending: zero borrow");
        _accrueInterest(msg.sender);
        UserAccount storage acct = accounts[msg.sender];

        uint256 newDebt = acct.borrowed + amount;
        require(
            acct.deposited * 100 >= newDebt * COLLATERAL_RATIO,
            "Lending: insufficient collateral"
        );

        acct.borrowed = newDebt;
        totalBorrowed += amount;

        bool ok = NativeCalls.gasTransfer(address(this), msg.sender, amount, "");
        require(ok, "Lending: borrow transfer failed");

        emit Borrowed(msg.sender, amount);
    }

    function _repay(address user, uint256 amount) internal {
        _accrueInterest(user);
        UserAccount storage acct = accounts[user];

        uint256 repayAmount = amount > acct.borrowed ? acct.borrowed : amount;
        acct.borrowed -= repayAmount;
        totalBorrowed -= repayAmount;

        emit Repaid(user, repayAmount);
    }

    /// @notice Liquidate an undercollateralized borrower.
    function liquidate(address borrower) external {
        _accrueInterest(borrower);
        UserAccount storage acct = accounts[borrower];

        require(acct.borrowed > 0, "Lending: no debt");
        require(
            acct.deposited * 100 < acct.borrowed * COLLATERAL_RATIO,
            "Lending: not undercollateralized"
        );

        uint256 debt = acct.borrowed;
        uint256 collateralSeized = debt + (debt * LIQUIDATION_BONUS) / 100;
        if (collateralSeized > acct.deposited) {
            collateralSeized = acct.deposited;
        }

        acct.borrowed = 0;
        acct.deposited -= collateralSeized;
        totalBorrowed -= debt;
        totalDeposited -= collateralSeized;

        // Transfer seized collateral to liquidator.
        bool ok = NativeCalls.gasTransfer(address(this), msg.sender, collateralSeized, "");
        require(ok, "Lending: liquidation transfer failed");

        emit Liquidated(borrower, msg.sender, debt, collateralSeized);
    }

    /// @notice Accrue simple per-block interest on a user's debt.
    function _accrueInterest(address user) internal {
        UserAccount storage acct = accounts[user];
        if (acct.borrowed > 0 && acct.lastBlock < block.number) {
            uint256 blocks = block.number - acct.lastBlock;
            uint256 interest = (acct.borrowed * INTEREST_RATE_PER_BLOCK * blocks) / BASIS_POINTS;
            acct.borrowed += interest;
            totalBorrowed += interest;
        }
        acct.lastBlock = block.number;
    }


    /// @notice View a user's current debt including accrued interest.
    function getDebt(address user) external view returns (uint256) {
        UserAccount storage acct = accounts[user];
        if (acct.borrowed == 0) return 0;
        uint256 blocks = block.number > acct.lastBlock ? block.number - acct.lastBlock : 0;
        uint256 interest = (acct.borrowed * INTEREST_RATE_PER_BLOCK * blocks) / BASIS_POINTS;
        return acct.borrowed + interest;
    }

    /// @notice View a user's collateral ratio (scaled by 100).
    function getCollateralRatio(address user) external view returns (uint256) {
        UserAccount storage acct = accounts[user];
        if (acct.borrowed == 0) return type(uint256).max;
        return (acct.deposited * 100) / acct.borrowed;
    }
}