// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /** * @title EscrowDVP * @notice Vol I · Chapter 15 — Solidity DVP Smart Contract * ========================================================= * Atomic delivery-versus-payment using an on-chain escrow pattern. * * State machine: * Created → Active (seller deposits securities) * → Locked (buyer deposits cash) * → Settled (atomic release of both legs) * → Failed (deadline expired or operator cancel) * * Security assumptions: * - securityToken and cashToken are trusted ERC-20 contracts * - Only the designated seller/buyer addresses may deposit * - Settlement is irrevocable once in Settled state */ interface IERC20 { function transferFrom(address from, address to, uint256 amount) external returns (bool); function transfer(address to, uint256 amount) external returns (bool); } contract EscrowDVP { // ── State machine ───────────────────────────────────────────────────────── enum State { Created, Active, Locked, Settled, Failed } // ── Immutable parameters ────────────────────────────────────────────────── address public immutable seller; // delivers securities address public immutable buyer; // pays cash address public immutable operator; // CSD / platform operator IERC20 public immutable securityToken; // tokenised security (ERC-20) IERC20 public immutable cashToken; // stablecoin (ERC-20, e.g. EUR-C) string public isin; uint256 public immutable securityAmount; // quantity of security tokens uint256 public immutable cashAmount; // cash amount (stablecoin decimals) uint256 public immutable settlementDeadline; // Unix timestamp // ── Mutable state ───────────────────────────────────────────────────────── State public state; // ── Events ──────────────────────────────────────────────────────────────── event SecuritiesDeposited(address indexed seller, uint256 amount, uint256 timestamp); event CashDeposited(address indexed buyer, uint256 amount, uint256 timestamp); event Settled(address indexed settler, uint256 timestamp); event Failed(address indexed initiator, string reason, uint256 timestamp); // ── Modifiers ───────────────────────────────────────────────────────────── modifier onlyState(State expected) { require(state == expected, "EscrowDVP: invalid state transition"); _; } modifier beforeDeadline() { require(block.timestamp <= settlementDeadline, "EscrowDVP: deadline expired"); _; } // ── Constructor ─────────────────────────────────────────────────────────── /** * @param _seller Address of the securities seller * @param _buyer Address of the cash payer * @param _securityToken ERC-20 address of the tokenised security * @param _cashToken ERC-20 address of the stablecoin * @param _isin ISIN of the security (informational) * @param _securityAmount Number of security tokens to transfer * @param _cashAmount Cash amount in stablecoin base units * @param _settlementDeadline Unix timestamp after which the DVP can be cancelled */ constructor( address _seller, address _buyer, address _securityToken, address _cashToken, string memory _isin, uint256 _securityAmount, uint256 _cashAmount, uint256 _settlementDeadline ) { require(_seller != address(0) && _buyer != address(0), "EscrowDVP: zero address"); require(_securityAmount > 0 && _cashAmount > 0, "EscrowDVP: zero amount"); require(_settlementDeadline > block.timestamp, "EscrowDVP: deadline in past"); seller = _seller; buyer = _buyer; operator = msg.sender; securityToken = IERC20(_securityToken); cashToken = IERC20(_cashToken); isin = _isin; securityAmount = _securityAmount; cashAmount = _cashAmount; settlementDeadline = _settlementDeadline; state = State.Created; } // ── Step 2: Seller deposits securities ─────────────────────────────────── /** * @notice Seller transfers security tokens into this contract. * State transition: Created → Active. * @dev Caller must have approved this contract for securityAmount. */ function depositSecurities() external onlyState(State.Created) beforeDeadline { require(msg.sender == seller, "EscrowDVP: caller is not seller"); require( securityToken.transferFrom(seller, address(this), securityAmount), "EscrowDVP: security transfer failed" ); state = State.Active; emit SecuritiesDeposited(seller, securityAmount, block.timestamp); } // ── Step 3: Buyer deposits cash ─────────────────────────────────────────── /** * @notice Buyer transfers stablecoin into this contract. * State transition: Active → Locked. * @dev Caller must have approved this contract for cashAmount. */ function depositCash() external onlyState(State.Active) beforeDeadline { require(msg.sender == buyer, "EscrowDVP: caller is not buyer"); require( cashToken.transferFrom(buyer, address(this), cashAmount), "EscrowDVP: cash transfer failed" ); state = State.Locked; emit CashDeposited(buyer, cashAmount, block.timestamp); } // ── Step 4a: Atomic settlement ──────────────────────────────────────────── /** * @notice Atomically releases both legs: securities to buyer, cash to seller. * Can be called by either party or the operator once both are deposited. * State transition: Locked → Settled. * @dev If either transfer reverts the entire transaction reverts — * the DVP invariant is maintained by EVM atomicity. */ function settle() external onlyState(State.Locked) beforeDeadline { require( msg.sender == seller || msg.sender == buyer || msg.sender == operator, "EscrowDVP: unauthorized settler" ); // Both legs in one transaction — atomically or not at all require(securityToken.transfer(buyer, securityAmount), "EscrowDVP: security release failed"); require(cashToken.transfer(seller, cashAmount), "EscrowDVP: cash release failed"); state = State.Settled; emit Settled(msg.sender, block.timestamp); } // ── Step 4b: Cancel ─────────────────────────────────────────────────────── /** * @notice Cancel the DVP and return assets to original owners. * Callable after the settlement deadline or by the operator at any time. */ function cancel() external { require( state == State.Active || state == State.Locked, "EscrowDVP: not cancellable in current state" ); require( block.timestamp > settlementDeadline || msg.sender == operator, "EscrowDVP: deadline not passed and caller is not operator" ); State prevState = state; state = State.Failed; if (prevState == State.Active || prevState == State.Locked) { securityToken.transfer(seller, securityAmount); } if (prevState == State.Locked) { cashToken.transfer(buyer, cashAmount); } emit Failed(msg.sender, "cancelled", block.timestamp); } // ── View ────────────────────────────────────────────────────────────────── /** * @notice Returns the full status of the DVP contract. */ function getStatus() external view returns ( State _state, string memory _isin, uint256 _securityAmount, uint256 _cashAmount, uint256 _deadline, bool _expired ) { return ( state, isin, securityAmount, cashAmount, settlementDeadline, block.timestamp > settlementDeadline ); } }