Files
swift-hackathon-2026/StandardsBasedTransferControl.sol
anthony.nguyen 3496126253 Add Solidity proof-of-concept source
Add the self-contained Solidity 0.8.28 transfer-control prototype and supporting README for the Swift Hackathon 2026 submission.
2026-07-26 11:31:47 +00:00

361 lines
11 KiB
Solidity

pragma solidity ^0.8.28;
contract Owned {
address public owner;
modifier onlyOwner() {
require(msg.sender == owner, "ONLY_OWNER");
_;
}
constructor() {
owner = msg.sender;
}
function transferOwnership(address newOwner) external onlyOwner {
require(newOwner != address(0), "ZERO_OWNER");
owner = newOwner;
}
}
contract IdentityRegistry is Owned {
struct Party {
bytes32 partyId;
bytes32 accountId;
bytes32 jurisdiction;
bool verified;
bool eligible;
bool frozen;
}
mapping(address => Party) public parties;
event PartySet(
address indexed wallet,
bytes32 partyId,
bytes32 accountId,
bytes32 jurisdiction,
bool verified,
bool eligible,
bool frozen
);
function setParty(
address wallet,
bytes32 partyId,
bytes32 accountId,
bytes32 jurisdiction,
bool verified,
bool eligible,
bool frozen
) external onlyOwner {
parties[wallet] = Party({
partyId: partyId,
accountId: accountId,
jurisdiction: jurisdiction,
verified: verified,
eligible: eligible,
frozen: frozen
});
emit PartySet(wallet, partyId, accountId, jurisdiction, verified, eligible, frozen);
}
function setFrozen(address wallet, bool frozen) external onlyOwner {
parties[wallet].frozen = frozen;
emit PartySet(
wallet,
parties[wallet].partyId,
parties[wallet].accountId,
parties[wallet].jurisdiction,
parties[wallet].verified,
parties[wallet].eligible,
frozen
);
}
function getParty(address wallet) external view returns (Party memory) {
return parties[wallet];
}
}
contract RoleRegistry is Owned {
mapping(bytes32 => mapping(address => bool)) public hasRole;
event RoleSet(bytes32 indexed role, address indexed account, bool enabled);
function setRole(bytes32 role, address account, bool enabled) external onlyOwner {
hasRole[role][account] = enabled;
emit RoleSet(role, account, enabled);
}
}
contract PolicyRegistry is Owned {
struct Policy {
bool active;
bool lockupActive;
uint256 maxAutoApproveQuantity;
}
mapping(bytes32 => Policy) public policies;
event PolicySet(
bytes32 indexed policyId,
bool active,
bool lockupActive,
uint256 maxAutoApproveQuantity
);
function setPolicy(
bytes32 policyId,
bool active,
bool lockupActive,
uint256 maxAutoApproveQuantity
) external onlyOwner {
policies[policyId] = Policy({
active: active,
lockupActive: lockupActive,
maxAutoApproveQuantity: maxAutoApproveQuantity
});
emit PolicySet(policyId, active, lockupActive, maxAutoApproveQuantity);
}
function getPolicy(bytes32 policyId) external view returns (Policy memory) {
return policies[policyId];
}
}
contract TransferControlModule is Owned {
enum Decision {
Approved,
Rejected,
PendingReview
}
struct ControlRequest {
bytes32 instructionId;
bytes32 messageDefinitionId;
bytes32 lifecycleEvent;
bytes32 assetId;
bytes32 assetType;
address assetContract;
bytes32 networkId;
address fromWallet;
address toWallet;
uint256 quantity;
bytes32 policyId;
}
struct ControlDecision {
Decision decision;
bytes32 reasonCode;
bytes32 policyId;
bytes32 evidenceRef;
}
bytes32 public constant SECONDARY_TRANSFER = keccak256("SECONDARY_TRANSFER");
bytes32 public constant APPROVED = keccak256("APPROVED");
bytes32 public constant FROM_NOT_VERIFIED = keccak256("FROM_NOT_VERIFIED");
bytes32 public constant TO_NOT_VERIFIED = keccak256("TO_NOT_VERIFIED");
bytes32 public constant PARTY_FROZEN = keccak256("PARTY_FROZEN");
bytes32 public constant ASSET_NOT_ALLOWED = keccak256("ASSET_NOT_ALLOWED");
bytes32 public constant LOCKUP_ACTIVE = keccak256("LOCKUP_ACTIVE");
bytes32 public constant POLICY_BLOCKED = keccak256("POLICY_BLOCKED");
bytes32 public constant REVIEW_REQUIRED = keccak256("REVIEW_REQUIRED");
bytes32 public constant UNSUPPORTED_LIFECYCLE_EVENT = keccak256("UNSUPPORTED_LIFECYCLE_EVENT");
IdentityRegistry public identityRegistry;
PolicyRegistry public policyRegistry;
constructor(address identityRegistry_, address policyRegistry_) {
identityRegistry = IdentityRegistry(identityRegistry_);
policyRegistry = PolicyRegistry(policyRegistry_);
}
function evaluate(ControlRequest calldata request)
external
view
returns (ControlDecision memory)
{
if (request.lifecycleEvent != SECONDARY_TRANSFER) {
return _decision(Decision.Rejected, UNSUPPORTED_LIFECYCLE_EVENT, request.policyId);
}
IdentityRegistry.Party memory fromParty = identityRegistry.getParty(request.fromWallet);
IdentityRegistry.Party memory toParty = identityRegistry.getParty(request.toWallet);
PolicyRegistry.Policy memory policy = policyRegistry.getPolicy(request.policyId);
if (!fromParty.verified) {
return _decision(Decision.Rejected, FROM_NOT_VERIFIED, request.policyId);
}
if (!toParty.verified) {
return _decision(Decision.Rejected, TO_NOT_VERIFIED, request.policyId);
}
if (fromParty.frozen || toParty.frozen) {
return _decision(Decision.Rejected, PARTY_FROZEN, request.policyId);
}
if (!toParty.eligible) {
return _decision(Decision.Rejected, ASSET_NOT_ALLOWED, request.policyId);
}
if (!policy.active) {
return _decision(Decision.Rejected, POLICY_BLOCKED, request.policyId);
}
if (policy.lockupActive) {
return _decision(Decision.Rejected, LOCKUP_ACTIVE, request.policyId);
}
if (request.quantity > policy.maxAutoApproveQuantity) {
return _decision(Decision.PendingReview, REVIEW_REQUIRED, request.policyId);
}
return _decision(Decision.Approved, APPROVED, request.policyId);
}
function _decision(Decision decision, bytes32 reasonCode, bytes32 policyId)
internal
pure
returns (ControlDecision memory)
{
return ControlDecision({
decision: decision,
reasonCode: reasonCode,
policyId: policyId,
evidenceRef: keccak256("SYNTHETIC_EVIDENCE")
});
}
}
contract TokenizedBond is Owned {
string public name;
string public symbol;
uint8 public decimals = 0;
uint256 public totalSupply;
bytes32 public assetId;
bytes32 public assetType;
bytes32 public messageDefinitionId;
bytes32 public networkId;
bytes32 public policyId;
bytes32 public constant TRANSFER_AGENT_ROLE = keccak256("TRANSFER_AGENT_ROLE");
bytes32 public constant SECONDARY_TRANSFER = keccak256("SECONDARY_TRANSFER");
bytes32 public constant INSUFFICIENT_POSITION = keccak256("INSUFFICIENT_POSITION");
TransferControlModule public controlModule;
RoleRegistry public roleRegistry;
mapping(address => uint256) public balanceOf;
event Transfer(address indexed from, address indexed to, uint256 value);
event TransferControlDecision(
bytes32 indexed instructionId,
address indexed fromWallet,
address indexed toWallet,
uint8 decision,
bytes32 reasonCode,
bytes32 policyId,
bytes32 messageDefinitionId,
bytes32 assetType,
bytes32 networkId
);
modifier onlyTransferAgent() {
require(
msg.sender == owner || roleRegistry.hasRole(TRANSFER_AGENT_ROLE, msg.sender),
"ONLY_TRANSFER_AGENT"
);
_;
}
constructor(
string memory name_,
string memory symbol_,
bytes32 assetId_,
bytes32 assetType_,
bytes32 messageDefinitionId_,
bytes32 networkId_,
bytes32 policyId_,
address controlModule_,
address roleRegistry_,
address initialHolder,
uint256 initialSupply
) {
name = name_;
symbol = symbol_;
assetId = assetId_;
assetType = assetType_;
messageDefinitionId = messageDefinitionId_;
networkId = networkId_;
policyId = policyId_;
controlModule = TransferControlModule(controlModule_);
roleRegistry = RoleRegistry(roleRegistry_);
balanceOf[initialHolder] = initialSupply;
totalSupply = initialSupply;
emit Transfer(address(0), initialHolder, initialSupply);
}
function controlledTransfer(
bytes32 instructionId,
address fromWallet,
address toWallet,
uint256 quantity
) external onlyTransferAgent returns (uint8, bytes32) {
if (balanceOf[fromWallet] < quantity) {
emit TransferControlDecision(
instructionId,
fromWallet,
toWallet,
uint8(TransferControlModule.Decision.Rejected),
INSUFFICIENT_POSITION,
policyId,
messageDefinitionId,
assetType,
networkId
);
return (uint8(TransferControlModule.Decision.Rejected), INSUFFICIENT_POSITION);
}
TransferControlModule.ControlRequest memory request =
TransferControlModule.ControlRequest({
instructionId: instructionId,
messageDefinitionId: messageDefinitionId,
lifecycleEvent: SECONDARY_TRANSFER,
assetId: assetId,
assetType: assetType,
assetContract: address(this),
networkId: networkId,
fromWallet: fromWallet,
toWallet: toWallet,
quantity: quantity,
policyId: policyId
});
TransferControlModule.ControlDecision memory decision = controlModule.evaluate(request);
if (decision.decision == TransferControlModule.Decision.Approved) {
balanceOf[fromWallet] -= quantity;
balanceOf[toWallet] += quantity;
emit Transfer(fromWallet, toWallet, quantity);
}
emit TransferControlDecision(
instructionId,
fromWallet,
toWallet,
uint8(decision.decision),
decision.reasonCode,
decision.policyId,
messageDefinitionId,
assetType,
networkId
);
return (uint8(decision.decision), decision.reasonCode);
}
}